//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using UnityEngine;

using Flare.Events;
using Flare.Geom;
using Flare.Script;
using Flare.Sys;
using Flare.Text;
using Flare.UI;

namespace Flare.Display
{
    /// <summary>
    /// The NativeWindow class provides an interface for holding instances of the
    /// Stage class. Multiple instances of the NativeWindow class can be created to
    /// support independent SWF file execution and rendering. Unlike AS3 this class
    /// does not create native platform windows, but instead allows viewport areas
    /// to be defined within a camera for SWF rendering.
    ///
    /// An important function of the NativeWindow class is to specify which instance
    /// is active for script execution via the Flare interface. For example, when
    /// adding event listeners to objects the window the EventDispatcher is associated
    /// with must be activated to ensure the events are properly registered. Flare
    /// assists with this by ensuring the proper window is active for scripting during
    /// script and event listener execution; however, if Flare objects are created
    /// elsewhere then the application must explicitly call the ActivateForScripts
    /// methods to ensure the proper window is active.
    /// </summary>
    public class NativeWindow : EventDispatcher
    {
        /// <summary>
        /// Current window active for script execution or null
        /// </summary>
        public static NativeWindow windowActiveForScripts { get; private set; }

        /// <summary>
        /// Indicates whether the window is active for receiving keyboard and mouse events.
        /// </summary>
        public bool active { get; private set; }

        /// <summary>
        /// Indicates whether the window is active for script execution
        /// </summary>
        public bool activeForScripts { get; private set; }

        /// <summary>
        /// ApplicationDomain for this window and any SWF definitions associated with it
        /// </summary>
        public ApplicationDomain applicationDomain { get; private set; }

        /// <summary>
        /// Size and location of the window. The bounds of the native window are in general
        /// managed by another object such as the SwfCameraOverlayPlayer, so it is unlikely
        /// the bounds will be set by other code.
        /// </summary>
        public Rectangle bounds {
            get { return m_bounds; }
            set
            {
                Rectangle prev = this.m_bounds;
                this.m_bounds = value;

                if ((prev.x != this.m_bounds.x) || (prev.y != this.m_bounds.y))
                {
                    // TODO bwmott 2013-03-09: Dispatch move event
                }

                if ((prev.width != this.m_bounds.width) || (prev.height != this.m_bounds.height))
                {
                    // TODO bwmott 2013-03-09: Dispatch resize event
                }
            }
        }
        private Rectangle m_bounds;

        /// <summary>
        /// Stage associated with the window
        /// </summary>
        public Stage stage { get; private set; }

        /// <summary>
        /// Indicates whether the window is transparent or not (i.e., is the background rendered)
        /// </summary>
        public bool transparent { get; set; }

        /// <summary>
        /// Indicates whether the window is visible or not
        /// </summary>
        public bool visible { get; set; }

        /// <summary>
        /// Height of the window in pixels
        /// </summary>
        public float height
        {
            get { return this.m_bounds.height; }
        }

        /// <summary>
        /// Width of the window in pixels
        /// </summary>
        public float width
        {
            get { return this.m_bounds.width; }
        }

        /// <summary>
        /// Horizontal coordinate of the window in pixels
        /// </summary>
        public float x
        {
            get { return this.m_bounds.x; }
        }

        /// <summary>
        /// Vertical coordinate of the window in pixels
        /// </summary>
        public float y
        {
            get { return this.m_bounds.y; }
        }

        internal AS2Queue as2Queue { get; private set; }

        internal DispatcherQueue dispatcherQueue { get; private set; }

        private InputAccessor inputAccessor { get; set; }

        private float nextUpdateTime;

        /// <summary>
        /// Create a new NativeWindow instance to display the SWF content and
        /// activate the window for scripts
        /// </summary>
        public NativeWindow(Rectangle bounds, bool transparent, byte[] swfBytes,
            InputAccessor inputAccessor)
        {
            this.applicationDomain = new ApplicationDomain();
            this.as2Queue = new AS2Queue();
            this.dispatcherQueue = new DispatcherQueue();
            this.nextUpdateTime = 0.0f;
            
            this.m_bounds = bounds;
            this.transparent = transparent;

            this.ActivateForScripts();

            this.inputAccessor = (inputAccessor == null) ? new InputAccessor() : inputAccessor;

            if (swfBytes != null)
            {
                Loader loader = new Flare.Display.Loader();
                loader.Load(swfBytes, new LoaderContext(ApplicationDomain.currentDomain));

                this.stage = new Stage(this,
                    loader.contentLoaderInfo.width, loader.contentLoaderInfo.height);
                this.stage.color = loader.contentLoaderInfo.color;
                this.stage.frameRate = loader.contentLoaderInfo.frameRate;
                this.stage.AddChild(loader.content);
                this.stage.loaderInfo = loader.contentLoaderInfo;
                loader.content.name = "root1";
            }
            else
            {
                this.stage = new Stage(this, this.bounds.width, this.bounds.height);
                this.stage.color = 0x00ffffff;
            }
        }

        /// <summary>
        /// Create a new NativeWindow instance to display the SWF content and
        /// activate the window for scripts
        /// </summary>
        public NativeWindow(Rectangle bounds, bool transparent, byte[] swfBytes)
            : this(bounds, transparent, swfBytes, null)
        {
        }

        /// <summary>
        /// Activates this window so that it is visible and receives focus
        /// </summary>
        public void Activate()
        {
            this.visible = true;
            this.active = true;
        }

        /// <summary>
        /// Activates this window for script execution
        /// </summary>
        public void ActivateForScripts()
        {
            if (windowActiveForScripts != null)
            {
                windowActiveForScripts.activeForScripts = false;
            }

            this.activeForScripts = true;
            NativeWindow.windowActiveForScripts = this;
        }

        /// <summary>
        /// Closes this window
        /// </summary>
        public void Close()
        {
            // TODO bwmott 2013-03-09: Consider dispatching close events

            this.visible = false;
            this.active = false;
            this.stage = null;

            if (this.activeForScripts)
            {
                NativeWindow.windowActiveForScripts = null;
                this.activeForScripts = false;
            }
        }

        public delegate void FSCommandCallback(string command, string parameters);
        internal FSCommandCallback fsCommandCallback;

        /// <summary>
        /// Adds the specified FSCommand callback so that it is executed whenever an FSCommand is
        /// encountered during script execution.
        /// </summary>
        public void AddFSCommandCallback(FSCommandCallback callback)
        {
            fsCommandCallback += callback;
        }

        /// <summary>
        /// Removes the specified FSCommand callback.
        /// </summary>
        public void RemoveFSCommandCallback(FSCommandCallback callback)
        {
            fsCommandCallback -= callback;
        }

        public void Update(float time)
        {
            // Activate this window so any listener or frame scripts execute in the correct context
            ActivateForScripts();

            if (time >= nextUpdateTime)
            {
                nextUpdateTime = time + (1.0f / stage.frameRate);

                // TODO bwmott 2013-03-16: Update MovieClip should be modified so that it does
                // not actually create the new children, instead it should make space for them
                // in the display list.
                UpdateMovieClips(stage);
    
                // Ensure the transforms for all of the disply list objects are updated
                UpdateTransforms(stage);
    
                this.dispatcherQueue.DispatchBroadcastEvent(Flare.Events.Event.ENTER_FRAME);
    
                // TODO bwmott 2013-03-16: Here we should actually create the new children and
                // place them in the display list and update their transforms.
    
                this.dispatcherQueue.DispatchBroadcastEvent(Flare.Events.Event.FRAME_CONSTRUCTED);

                // TODO bwmott 2013-03-16: Here we should execute any frame scripts which have
                // been added via MovieClip.addFrameScript().

                // Execute any AS2 scripts
                ExecuteAS2Scripts();
    
                // Dispatch FRAME_CONSTRUCTED event
                this.dispatcherQueue.DispatchBroadcastEvent(Flare.Events.Event.EXIT_FRAME);
            }
            else
            {
                // Ensure the transforms for all of the disply list objects are updated
                UpdateTransforms(stage);
            }

            // Process any user interaction events
            DispatchUserInteractionEvents();

            // Update any sprite that's being dragged
            UpdateSpriteDragging();
        }

        internal void ExecuteAS2Scripts()
        {
            // TODO bwmott 2013-08-12: Consider putting a cutoff on the number of scripts that
            // can be executed to handle runaway scripts
            for(;;)
            {
                AS2 as2;
                DisplayObject target;
                if (NativeWindow.windowActiveForScripts.as2Queue.TryDequeue(out as2, out target))
                {
                    as2.Execute(target, NativeWindow.windowActiveForScripts.stage);
                }
                else
                {
                    break;
                }
            }
        }

        internal static void UpdateMovieClips(DisplayObject obj)
        {
            if (obj is MovieClip)
            {
                ((MovieClip)obj).Update();
            }

            if (obj is DisplayObjectContainer)
            {
                DisplayObjectContainer container = (DisplayObjectContainer)obj;
                for (int i = 0; i < container.numChildren; ++i)
                {
                    UpdateMovieClips(container.GetChildAt(i));
                }
            }
        }

        private static void UpdateTransforms(DisplayObject obj, Matrix parentMatrix = null,
            ColorTransform parentColorTransform = null)
        {
            obj.transform.concatenatedMatrix.CopyFrom(obj.transform.matrix);
            if (parentMatrix != null)
            {
                obj.transform.concatenatedMatrix.Concat(parentMatrix);
            }

            if (parentColorTransform != null)
            {
                obj.transform.concatenatedColorTransform.CopyFrom(parentColorTransform);
            }
            else
            {
                obj.transform.concatenatedColorTransform.Identity();
            }
            obj.transform.concatenatedColorTransform.Concat(obj.transform.colorTransform);

            if (obj is DisplayObjectContainer)
            {
                DisplayObjectContainer container = (DisplayObjectContainer)obj;
                for (int i = 0; i < container.numChildren; ++i)
                {
                    UpdateTransforms(container.GetChildAt(i), obj.transform.concatenatedMatrix,
                        obj.transform.concatenatedColorTransform);
                }
            }
        }

        private static Material ms_solidMaterial = null;

        // Render context object that used during rendering of the SWF
        private DisplayObject.RenderContext m_renderContext = new DisplayObject.RenderContext();

        public void Render()
        {
            // Activate this window so any listener or frame scripts execute in the correct context
            ActivateForScripts();

            // Create rendering material if it hasn't been created
            if (ms_solidMaterial == null)
            {
                Shader solidShader = Shader.Find("Flare/SolidFill");
                if (solidShader == null)
                {
                    Log.Error(Subsystem.Playback, "Could not find 'Flare/SolidFill' shader. " +
                        "Please ensure it is in a Resource folder.");
                }
                else
                {
                    ms_solidMaterial = new Material(solidShader);
                }
            }

            // TODO bwmott 2013-03-16: Here is where we should dispatch the RENDER event
            // if we decide to support it.

            // TODO bwmott 2013-03-09: Need to adjust rendering based on the bounds of
            // the window, stage alignment, and stage scale mode

            // Ensure the transforms for all of the disply list objects are updated
            UpdateTransforms(stage);

            // Set up projection matrix and viewport for rendering
            GL.PushMatrix();
            GL.LoadPixelMatrix(0, stage.originalStageWidth - 1, stage.originalStageHeight - 1, 0);
            GL.Viewport((Rect)this.bounds);

            // Clear the viewport to the background color if we're not transparent
            if (!this.transparent)
            {
                uint color = stage.color;
                ms_solidMaterial.color = new Color32((byte)(color >> 16), (byte)(color >> 8),
                    (byte)color, 0xff);
                if (ms_solidMaterial.SetPass(0))
                {
                    float x1 = 0.0f;
                    float y1 = 0.0f;
                    float x2 = stage.originalStageWidth - 1.0f;
                    float y2 = stage.originalStageHeight - 1.0f;

                    GL.Begin(GL.QUADS);
                    GL.Vertex3(x1, y1, 0.0f);
                    GL.Vertex3(x2, y1, 0.0f);
                    GL.Vertex3(x2, y2, 0.0f);
                    GL.Vertex3(x1, y2, 0.0f);
                    GL.End();
                }
            }

            m_renderContext.Reset();
            m_renderContext.stageTranslationAndScale.SetTo(
                this.bounds.width / this.stage.originalStageWidth, 0.0f, 
                0.0f, this.bounds.height / this.stage.originalStageHeight,
                this.bounds.x, this.bounds.y);

            this.stage.Render(this.m_renderContext);

            // Restore rendering state
            GL.PopMatrix();
        }

        private void DispatchUserInteractionEvents()
        {
            // Only dispatch keyboard and mouse events if the native window is active
            if (this.active)
            {
                // Handle dispatching any keyboard events
                DispatchKeyboardEvents();
                
                // Handle dispatching any mouse events
                DispatchMouseEvents();
            }
        }

        //--------------------------------------------------------------------------------

        private List<Keyboard.KeyCodeMap> monitorKeyReleaseList = new List<Keyboard.KeyCodeMap>();

        private KeyCode repeatUnityKeyCode = 0;
        private float repeatStartTime = 0.0f;
        private float repeatDelay = 0.0f;

        private void DispatchKeyboardEvents()
        {
            // Get the state of the modifier keys
            bool altKey = inputAccessor.GetKey(KeyCode.LeftAlt) ||
                inputAccessor.GetKey(KeyCode.RightAlt);
            bool ctrlKey = inputAccessor.GetKey(KeyCode.LeftControl) ||
                inputAccessor.GetKey(KeyCode.RightControl) ||
                inputAccessor.GetKey(KeyCode.LeftCommand) ||
                inputAccessor.GetKey(KeyCode.RightCommand);
            bool shiftKey = inputAccessor.GetKey(KeyCode.LeftShift) ||
                inputAccessor.GetKey(KeyCode.RightShift);

            // If any key has been pressed see if we should generate a keyDown event
            if (inputAccessor.anyKeyDown)
            {
                for (int i = 0; i < Keyboard.KeyCodeMaps.Length; ++i)
                {
                    var entry = Keyboard.KeyCodeMaps[i];
                    if (inputAccessor.GetKeyDown(entry.unityCode))
                    {
                        repeatUnityKeyCode = entry.unityCode;
                        repeatStartTime = Time.realtimeSinceStartup;
                        repeatDelay = 0.5f;

                        DisplayObject target = (this.stage.focus != null) ?
                            this.stage.focus : this.stage;

                        target.DispatchEvent(new KeyboardEvent(KeyboardEvent.KEY_DOWN,
                            true, false, shiftKey ? entry.shiftedCharCode: entry.charCode, 
                            entry.flareCode, entry.keyLocation, ctrlKey, altKey, shiftKey));
                         
                        this.monitorKeyReleaseList.Add(entry);
                    }
                }
            }
            
            for (int i = 0; i < this.monitorKeyReleaseList.Count; )
            {
                var entry = this.monitorKeyReleaseList[i];
                if (!inputAccessor.GetKey(entry.unityCode))
                {
                    DisplayObject target = (this.stage.focus != null) ?
                        this.stage.focus : this.stage;

                    target.DispatchEvent(new KeyboardEvent(KeyboardEvent.KEY_UP,
                        true, false, shiftKey ? entry.shiftedCharCode : entry.charCode, 
                        entry.flareCode, entry.keyLocation, ctrlKey, altKey, shiftKey));

                    this.monitorKeyReleaseList.RemoveAt(i);

                    if (repeatUnityKeyCode == entry.unityCode)
                    {
                        repeatUnityKeyCode = 0;
                    }
                }
                else
                {
                    // See if we should dispatch another key down event for repeating keys
                    if ((repeatUnityKeyCode == entry.unityCode) &&
                        (Time.realtimeSinceStartup > repeatStartTime + repeatDelay))
                    {
                        repeatStartTime = Time.realtimeSinceStartup;
                        repeatDelay = 0.075f;

                        DisplayObject target = (this.stage.focus != null) ?
                            this.stage.focus : this.stage;
                        
                        target.DispatchEvent(new KeyboardEvent(KeyboardEvent.KEY_DOWN,
                            true, false, shiftKey ? entry.shiftedCharCode: entry.charCode, 
                            entry.flareCode, entry.keyLocation, ctrlKey, altKey, shiftKey));
                    }

                    ++i;
                }
            }
        }
        
        //--------------------------------------------------------------------------------

        private Vector2 previousStageMousePosition = new Vector2(System.Single.NaN,
            System.Single.NaN);

        private void DispatchMouseEvents()
        {
            // Get the state of the modifier keys
            bool altKey = inputAccessor.GetKey(KeyCode.LeftAlt) ||
                inputAccessor.GetKey(KeyCode.RightAlt);
            bool ctrlKey = inputAccessor.GetKey(KeyCode.LeftControl) ||
                inputAccessor.GetKey(KeyCode.RightControl) ||
                inputAccessor.GetKey(KeyCode.LeftCommand) ||
                inputAccessor.GetKey(KeyCode.RightCommand);
            bool shiftKey = inputAccessor.GetKey(KeyCode.LeftShift) ||
                inputAccessor.GetKey(KeyCode.RightShift);

            // TODO bwmott 2013-03-17: Need to adjust x & y based on stage alignment
            // and stage scale mode
            float stageX = ((float)inputAccessor.mousePosition.x - this.bounds.x) /
                (float)this.bounds.width;
            float stageY = 1.0f - (((float)inputAccessor.mousePosition.y - this.bounds.y) / 
                (float)this.bounds.height);
            stageX = stageX * stage.stageWidth;
            stageY = stageY * stage.stageHeight;

            // Handle MOUSE_DOWN event
            if (inputAccessor.GetMouseButtonDown(0) &&
                dispatcherQueue.HasListeners(MouseEvent.MOUSE_DOWN))
            {
                DisplayObject target = LocateHitTarget(stage, stageX, stageY, true);
                if (target != null)
                {
                    Point local = target.GlobalToLocal(new Point(stageX, stageY));
                    target.DispatchEvent(new MouseEvent(MouseEvent.MOUSE_DOWN, true, false,
                        local.x, local.y, stageX, stageY, null, ctrlKey, altKey, shiftKey, true));
                }
            }

            // Handle MOUSE_UP event
            if (inputAccessor.GetMouseButtonUp(0) &&
                dispatcherQueue.HasListeners(MouseEvent.MOUSE_UP))
            {
                DisplayObject target = LocateHitTarget(stage, stageX, stageY, true);
                if (target != null)
                {
                    Point local = target.GlobalToLocal(new Point(stageX, stageY));
                    target.DispatchEvent(new MouseEvent(MouseEvent.MOUSE_UP, true, false,
                        local.x, local.y, stageX, stageY, null, ctrlKey, altKey, shiftKey, true));
                }
            }

            // Handle RIGHT_MOUSE_DOWN event
            if (inputAccessor.GetMouseButtonDown(1) &&
                dispatcherQueue.HasListeners(MouseEvent.RIGHT_MOUSE_DOWN))
            {
                DisplayObject target = LocateHitTarget(stage, stageX, stageY, true);
                if (target != null)
                {
                    Point local = target.GlobalToLocal(new Point(stageX, stageY));
                    target.DispatchEvent(new MouseEvent(MouseEvent.RIGHT_MOUSE_DOWN, true, false,
                        local.x, local.y, stageX, stageY, null, ctrlKey, altKey, shiftKey, true));
                }
            }

            // Handle RIGHT_MOUSE_UP event
            if (inputAccessor.GetMouseButtonUp(1) &&
                dispatcherQueue.HasListeners(MouseEvent.RIGHT_MOUSE_UP))
            {
                DisplayObject target = LocateHitTarget(stage, stageX, stageY, true);
                if (target != null)
                {
                    Point local = target.GlobalToLocal(new Point(stageX, stageY));
                    target.DispatchEvent(new MouseEvent(MouseEvent.RIGHT_MOUSE_UP, true, false,
                        local.x, local.y, stageX, stageY, null, ctrlKey, altKey, shiftKey, false));
                }
            }

            // Handle MOUSE_MOVE event
            if (((previousStageMousePosition.x != stageX) ||
                (previousStageMousePosition.y != stageY)) &&
                dispatcherQueue.HasListeners(MouseEvent.MOUSE_MOVE))
            {
                DisplayObject target = LocateHitTarget(stage, stageX, stageY, true);
                if (target != null)
                {
                    Point local = target.GlobalToLocal(new Point(stageX, stageY));
                    target.DispatchEvent(new MouseEvent(MouseEvent.MOUSE_MOVE, true, false,
                        local.x, local.y, stageX, stageY, null, ctrlKey, altKey, shiftKey,
                        inputAccessor.GetMouseButtonDown(0)));
                }
            }

            previousStageMousePosition = new Vector2(stageX, stageY);
        }

        internal static DisplayObject LocateHitTarget(DisplayObject obj,
            float x, float y, bool shapeFlag)
        {
            if (!obj.visible)
            {
                return null;
            }

            // Recurse into the children looking for hits
            if (obj is DisplayObjectContainer)
            {
                DisplayObjectContainer container = obj as DisplayObjectContainer;
                for (int i = container.m_children.Count - 1; i >= 0; --i)
                {
                    var child = container.m_children[i];

                    DisplayObject hit = LocateHitTarget(child, x, y, shapeFlag);
                    if (hit != null)
                    {
                        return hit;
                    }
                }
            }

            // At this point, we have evaluated any children for a hit so we do not evaluate
            // the children again since we know there is no hit with them
            if (obj.HitTest(x, y, shapeFlag, false))
            {
                return obj;
            }
            else
            {
                return null;
            }
        }

        //--------------------------------------------------------------------------------

        private Sprite draggingSprite { get; set; }

        internal void StartSpriteDrag(Sprite sprite, bool lockCenter, Rectangle? bounds)
        {
            this.draggingSprite = sprite;
        }

        internal void StopSpriteDrag()
        {
            this.draggingSprite = null;
        }

        internal void UpdateSpriteDragging()
        {
            if (this.draggingSprite != null)
            {
                // TODO bwmott 2013-03-17: Need to adjust x & y based on stage alignment
                // and stage scale mode
                float x = ((float)inputAccessor.mousePosition.x - this.bounds.x) /
                    (float)this.bounds.width;
                float y = 1.0f - (((float)inputAccessor.mousePosition.y - this.bounds.y) / 
                    (float)this.bounds.height);
                x = x * stage.stageWidth;
                y = y * stage.stageHeight;

                // Convert to parent's local coordinates
                if (this.draggingSprite.parent != null)
                {
                    Point local = this.draggingSprite.parent.GlobalToLocal(new Point(x, y));
                    this.draggingSprite.x = local.x;
                    this.draggingSprite.y = local.y;
                }
                else
                {
                    this.draggingSprite.x = x;
                    this.draggingSprite.y = y;
                }
            }
        }
       
        //--------------------------------------------------------------------------------

        internal class DispatcherQueue
        {
            private Dictionary<string, List<global::System.WeakReference>> queue { get; set; }

            public DispatcherQueue()
            {
                this.queue = new Dictionary<string, List<global::System.WeakReference>>();
            }

            public void Add(string type, EventDispatcher obj)
            {
                // Ensure there's a queue for this type of event
                if (!queue.ContainsKey(type))
                {
                    queue.Add(type, new List<global::System.WeakReference>());
                }

                // See if the EventDispatcher is already in the queue
                var list = queue[type];
                foreach (var entry in list)
                {
                    if (entry.Target == obj)
                    {
                        // Yes, it is so we're done
                        return;
                    }
                }

                // No, it isn't in the queue, so let's add a weak reference to it
                list.Add(new global::System.WeakReference(obj));
            }

            public void Remove(string type, EventDispatcher obj)
            {
                if (queue.ContainsKey(type))
                {
                    // Remove the EventDispatcher if it is in the queue
                    var list = queue[type];
                    for (int i = 0; i < list.Count; ++i)
                    {
                        if (list[i].Target == obj)
                        {
                            list.RemoveAt(i);
                            return;
                        }
                    }
                }
            }

            public bool HasListeners(string type)
            {
                return queue.ContainsKey(type);
            }

            public void DispatchBroadcastEvent(string eventType)
            {
                var list = this.ActiveListeners(eventType);
                if (list != null)
                {
                    for (int i = 0; i < list.Count; ++i)
                    {
                        EventDispatcher obj = list[i].Target as EventDispatcher;
                        if (obj != null)
                        {
                            Flare.Events.Event evt = new Flare.Events.Event(eventType);
                            obj.DispatchEventAtTargetPhase(evt);
                        }
                    }
                }
            }
          
            private List<global::System.WeakReference> ActiveListeners(string type)
            {
                List<global::System.WeakReference> result = null;
    
                if (queue.ContainsKey(type))
                {
                    // Get the list of dispatchers
                    result = queue[type];

                    // Discard any "dead" dispatchers from the list
                    for (int i = 0; i < result.Count; )
                    {
                        object obj = result[i].Target;
                        if (obj == null)
                        {
                            result.RemoveAt(i);
                        }
                        else
                        {
                            ++i;
                        }
                    }
                }
                
                return result;
            }
        }

        //--------------------------------------------------------------------------------

        internal class AS2Queue
        {
            private List<KeyValuePair<AS2, DisplayObject>> queue;

            public AS2Queue()
            {
                this.queue = new List<KeyValuePair<AS2, DisplayObject>>();
            }

            public void Enqueue(AS2 as2, DisplayObject target)
            {
                this.queue.Add(new KeyValuePair<AS2, DisplayObject>(as2, target));
            }

            public bool TryDequeue(out AS2 as2, out DisplayObject target)
            {
                if (this.queue.Count > 0)
                {
                    as2 = this.queue[0].Key;
                    target = this.queue[0].Value;
                    this.queue.RemoveAt(0);

                    return true;
                }
                else
                {
                    as2 = null;
                    target = null;

                    return false;
                }
            }
        }
    }
}
